Skip to content

fix: safely render structured output objects to prevent React error #31#3483

Open
MaxwellCalkin wants to merge 2 commits intosimstudioai:mainfrom
MaxwellCalkin:fix/react-error-31-object-rendering
Open

fix: safely render structured output objects to prevent React error #31#3483
MaxwellCalkin wants to merge 2 commits intosimstudioai:mainfrom
MaxwellCalkin:fix/react-error-31-object-rendering

Conversation

@MaxwellCalkin
Copy link

Summary

Fixes #2725

When LLM providers (especially Anthropic) return structured content blocks like { text: "...", type: "text" } or arrays of content blocks [{ text: "...", type: "text" }], the chat UI crashes with React error #31 ("Objects are not valid as a React child") because these objects are passed directly as React children.

This PR introduces a centralized safeRenderValue() utility that safely converts any value to a renderable string:

  • Extracts .text from { text, type } content block objects
  • Joins text from arrays of content blocks
  • Falls back to JSON.stringify for other objects
  • Passes through strings, numbers, and booleans unchanged

Changes

  • New: apps/sim/lib/core/utils/safe-render.ts — centralized utility function
  • New: apps/sim/lib/core/utils/safe-render.test.ts — 15 unit tests (all passing)
  • Fixed: apps/sim/app/chat/components/message/message.tsx — public deployed chat now uses safeRenderValue instead of rendering raw content
  • Fixed: apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx — floating workspace chat now uses safeRenderValue
  • Fixed: apps/sim/app/chat/hooks/use-chat-streaming.tsstopStreaming safely converts content before string concatenation
  • Fixed: apps/sim/stores/chat/store.tsappendMessageContent safely converts existing content before concatenation

Test plan

  • All 15 unit tests pass for safeRenderValue
  • Verify chat renders correctly when LLM returns { text: "...", type: "text" } content blocks
  • Verify chat renders correctly when LLM returns arrays of content blocks
  • Verify stopping a streaming response works when content is a structured object
  • Verify appending to a message works when existing content is a structured object
  • Verify normal string content continues to render unchanged

AI Disclosure: This PR was authored by Claude, an AI assistant made by Anthropic operating under the GitHub account of Max Calkin (@MaxwellCalkin). See our application post for context.

…imstudioai#31

When LLM providers (especially Anthropic) return structured content blocks
like { text, type } objects, the UI crashes with React error simstudioai#31 because
these objects are passed directly as React children instead of being
converted to strings first.

This adds a safeRenderValue() utility that:
- Extracts the .text property from { text, type } content blocks
- Joins text from arrays of content blocks (Anthropic format)
- Falls back to JSON.stringify for other objects
- Returns primitives unchanged

Applied to all chat message rendering paths:
- Public deployed chat (ClientChatMessage)
- Floating workspace chat (ChatMessage)
- Chat streaming hook (stopStreaming content concatenation)
- Chat store (appendMessageContent)

Fixes simstudioai#2725

AI Disclosure: This commit was authored by Claude Opus 4.6 (Anthropic), an AI agent operated by Maxwell Calkin (@MaxwellCalkin).
@cursor
Copy link

cursor bot commented Mar 9, 2026

PR Summary

Low Risk
Low risk UI/state handling change that normalizes chat content to strings; main risk is minor formatting differences (e.g., JSON/text joining) in rendered and copied message output.

Overview
Prevents chat UIs from crashing when LLMs return structured content (e.g. { text, type } blocks or arrays) by introducing a shared safeRenderValue() utility and unit tests.

Updates both chat message renderers plus streaming stop/append paths to always convert existing message.content to a safe string before rendering, copying to clipboard, or concatenating streamed text.

Written by Cursor Bugbot for commit 4443719. This will update automatically on new commits. Configure here.

@vercel
Copy link

vercel bot commented Mar 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 9, 2026 6:36am

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR introduces a centralized safeRenderValue() utility to fix React error #31, which occurred when LLM providers (particularly Anthropic) returned structured content blocks (e.g., { text: "...", type: "text" }) that were passed directly as React children instead of being converted to strings first.

Key changes:

  • New apps/sim/lib/core/utils/safe-render.tssafeRenderValue() extracts .text from content block objects, joins text from content block arrays, and falls back to JSON.stringify for other objects
  • New apps/sim/lib/core/utils/safe-render.test.ts — 15 unit tests covering all main branches
  • Fixed apps/sim/stores/chat/store.tsappendMessageContent now safely converts existing content before string concatenation
  • Fixed apps/sim/app/chat/hooks/use-chat-streaming.tsstopStreaming safely converts content before appending the "stopped" notice
  • Fixed apps/sim/app/workspace/.../chat-message/chat-message.tsx — workspace chat uses safeRenderValue directly (cleanest implementation)
  • Partially fixed apps/sim/app/chat/components/message/message.tsx — the React crash is resolved, but the isJsonObject branch uses JSON.stringify instead of safeRenderValue, causing Anthropic content blocks to display as formatted JSON rather than extracted text in the public chat, creating a UX inconsistency with the workspace chat

Confidence Score: 3/5

  • The React crash (error Edge connection logic #31) is correctly fixed across all surfaces. However, the UX goal of the PR is only partially achieved due to an inconsistency in message.tsx.
  • The core bug is properly fixed: all code paths safely convert content to strings before rendering or concatenating. The utility is well-tested and logically sound. However, the isJsonObject branch in message.tsx (public chat) bypasses safeRenderValue and uses raw JSON.stringify, causing Anthropic { text, type } content blocks to render as formatted JSON rather than extracted text. This creates a UX inconsistency where the same content displays differently across chat surfaces (extracted text in workspace chat, raw JSON in public chat), which contradicts the PR's stated intent to properly extract and display text from structured content blocks.
  • apps/sim/app/chat/components/message/message.tsx — the isJsonObject branch should use safeRenderValue to maintain consistent UX with the workspace chat surface

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[LLM Response\ncontent arrives] --> B{content type?}

    B -->|string| C[Pass through unchanged]
    B -->|null / undefined| D[Return empty string]
    B -->|number / boolean| E[String conversion]
    B -->|object with .text string| F[Extract .text value]
    B -->|array of content blocks| G[Map each item:\n- string → as-is\n- object with .text → extract text\n- other → JSON.stringify]
    B -->|other object| H[JSON.stringify fallback]

    C --> Z[safeRenderValue: string output]
    D --> Z
    E --> Z
    F --> Z
    G --> Z
    H --> Z

    Z --> R1[chat-message.tsx\nworkspace chat\nuses safeRenderValue directly]
    Z --> R2[message.tsx\npublic chat — non-object path\nuses safeRenderValue]
    Z --> R3[store.ts appendMessageContent\nuses safeRenderValue before concat]
    Z --> R4[use-chat-streaming.ts stopStreaming\nuses safeRenderValue before concat]

    X[message.tsx isJsonObject=true\npublic chat — object path] -->|JSON.stringify bypass| R2
    style X fill:#ffcccc,stroke:#cc0000
Loading

Last reviewed commit: e0dd596

Comment on lines +56 to +61
const cleanTextContent = useMemo(() => {
if (isJsonObject) {
return JSON.stringify(message.content, null, 2)
}
return safeRenderValue(message.content)
}, [message.content, isJsonObject])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistency: isJsonObject branch uses JSON.stringify instead of safeRenderValue

When isJsonObject is true (i.e., when a structured content block like { text: "Hello", type: "text" } is returned), cleanTextContent is computed via JSON.stringify, which displays the raw JSON structure in the <pre> block rather than extracting the .text field.

This is inconsistent with chat-message.tsx in the workspace surface, which uses safeRenderValue unconditionally and correctly extracts the .text field. As a result, the same content block displays differently across the two chat surfaces: raw JSON in public chat vs. extracted text in workspace chat.

Consider using safeRenderValue for both branches:

Suggested change
const cleanTextContent = useMemo(() => {
if (isJsonObject) {
return JSON.stringify(message.content, null, 2)
}
return safeRenderValue(message.content)
}, [message.content, isJsonObject])
// Safely convert content to a renderable string to prevent React error #31
// when LLM returns structured objects (e.g. { text, type })
const cleanTextContent = useMemo(() => {
return safeRenderValue(message.content)
}, [message.content])

If displaying formatted JSON for non-content-block objects is intentional, the isJsonObject flag can still control the <pre> vs <span> rendering decision without affecting text extraction.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

The isJsonObject branch was bypassing safeRenderValue and calling
JSON.stringify directly, which displayed raw JSON for structured
objects like { text, type } instead of extracting the text field.

Now all content goes through safeRenderValue which extracts .text
from content blocks and falls back to JSON.stringify only when
no text field is found.

Addresses review feedback from Greptile and Cursor bots.

AI Disclosure: This commit was authored by Claude Opus 4.6 (Anthropic),
an AI agent operated by Maxwell Calkin (@MaxwellCalkin).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"React error #31 when rendering workflow/node output objects ({text, type})”

1 participant